import { normalizePath, type Logger, type PluginOption, type ResolvedServerUrls } from "vite";
import { dirname, join, resolve } from "node:path";
import { existsSync } from "node:fs";
import { mkdir, readFile, rmdir, writeFile } from "node:fs/promises";
import fastGlob from "fast-glob";
import { type IncomingMessage, type ServerResponse } from "node:http";
import type { AcceptedPlugin, Rule } from "postcss";
import postcss from "postcss";
import colors from "picocolors";

export interface SourceProxyOptions {
  dir: string;
  headHtml?: string;
  bodyHtml?: string;
  /**
   * 默认的 pages 入口文件
   * @default "/index.html"
   */
  pageIndex?: string;
  /**
   * 需要插入 html 中的 js/ts 入口文件
   * @example /src/main.ts
   */
  entry?: string;
  /**
   * 清除 element-plus 的样式
   * @default true
   */
  cleanStyle?: boolean;
  /**
   * 启动时是否清除缓存目录
   * @default true
   */
  cleanTemp?: boolean;
  /**
   * 自动在 html 中生成 head/body/entry 的占位符
   * @default true
   */
  occupyPosition?: boolean;
  /**
   * 忽略的路径
   * @default ['/']
   */
  exclude?: string[];
}

const headOccupyPosition = "<!-- Head Occupy position -->";
const bodyOccupyPosition = "<!-- Body Occupy position -->";
const entryOccupyPosition = "<!-- Entry Occupy position -->";

export default function vitePagesProxy(options: SourceProxyOptions): PluginOption {
  options = { cleanStyle: true, cleanTemp: true, occupyPosition: true, pageIndex: "/index.html", exclude: ["/"], ...options };

  const baseDir = resolve(options.dir);
  const tempDir = resolve("./node_modules/.vite-pages-temp");

  let pages: string[] = [];
  const includeMap: Record<string, string> = {};
  const excludes = new Set<string>(options.exclude);

  function normalizeUrl(url: string) {
    const urls = [normalizePath(url), normalizePath(url + ".html"), normalizePath(join(url, "/index.html"))];
    for (let i = 0; i < urls.length; i++) {
      const exist = pages.includes(urls[i]);
      if (exist) {
        return { exist, target: urls[i] };
      }
    }
    return { target: url, exist: false };
  }

  async function writeTemp(tempPath: string, content: string) {
    const base = dirname(tempPath);
    if (!existsSync(base)) {
      await mkdir(base, { recursive: true });
    }
    writeFile(tempPath, content);
  }

  function normalizeType(url: string) {
    const postfix = url.split(".").pop();
    switch (postfix) {
      case "html":
        return { contentType: "text/html", isHtml: true, isCss: false, isJs: false, isImage: false };
      case "css":
        return { contentType: "text/css", isHtml: false, isCss: true, isJs: false, isImage: false };
      case "js":
        return { contentType: "text/javascript", isHtml: false, isCss: false, isJs: true, isImage: false };
      case "png":
        return { contentType: "image/png", isHtml: false, isCss: false, isJs: false, isImage: true };
      case "svg":
        return { contentType: "image/svg+xml", isHtml: false, isCss: false, isJs: false, isImage: true };
      default:
        return { contentType: "text/plain", isHtml: false, isCss: false, isJs: false, isImage: false };
    }
  }

  async function loadSource(url: string, res: ServerResponse<IncomingMessage>) {
    try {
      const { contentType, isHtml, isCss, isImage, isJs } = normalizeType(url);

      res.setHeader("Access-Control-Allow-Origin", "*");
      res.setHeader("Content-Type", contentType);

      const filePath = join(baseDir, url);
      const tempPath = join(tempDir, url);

      if (isImage || isJs) {
        const file = await readFile(filePath);
        res.end(file);
        return;
      }

      if (existsSync(tempPath)) {
        let temp = await readFile(tempPath, "utf-8");
        if (isHtml) {
          temp = normalizeHtml(temp, options);
        }
        res.end(temp);
        return;
      }

      let text = await readFile(filePath, "utf-8");
      if (isHtml) {
        if (options.occupyPosition) {
          text = text.replace("</head>", `${headOccupyPosition}</head>`).replace("</body>", `${bodyOccupyPosition}${entryOccupyPosition}</body>`);
        }
        writeTemp(tempPath, text);
        text = normalizeHtml(text, options);
      } else if (isCss) {
        if (options.cleanStyle) {
          text = await normalizeCss(text);
        }
        writeTemp(tempPath, text);
      }

      res.end(text);
    } catch (error) {
      res.end((error as Error).message || String(error));
    }
  }

  return {
    name: "pages-proxy",
    enforce: "pre",
    apply: "serve",
    config() {
      fastGlob.glob("**", { cwd: baseDir, onlyFiles: true, stats: false }).then((files) => {
        pages = files.map((file) => normalizePath(join("/", file)));
      });
      if (options.cleanTemp && existsSync(tempDir)) {
        rmdir(tempDir, { recursive: true });
      }
    },
    configureServer(server) {
      const _printUrls = server.printUrls;

      server.printUrls = () => {
        _printUrls();

        const urls = server.resolvedUrls!;
        const colorUrl = (url: string) => {
          url = url.replace(/:(\d+)\//, (_, port) => `:${colors.bold(port)}/`);
          return url.endsWith("/") ? url + options.pageIndex!.slice(1) : url + options.pageIndex;
        };
        for (const url of urls.local) {
          console.log(`  ${colors.green("➜")}  ${colors.bold("Local Pages Entry")}:   ${colors.cyan(colorUrl(url))}`);
        }
        for (const url of urls.network) {
          console.log(`  ${colors.green("➜")}  ${colors.bold("Network Pages Entry")}: ${colors.cyan(colorUrl(url))}`);
        }
      };

      server.middlewares.use((req, res, next) => {
        if (req.method === "GET") {
          const url = req.url || "";

          if (!excludes.has(url)) {
            if (includeMap[url]) {
              loadSource(url, res);
              return;
            }
            const { target, exist } = normalizeUrl(url);
            if (exist) {
              includeMap[url] = target;
              loadSource(target, res);
              return;
            }
            excludes.add(url);
          }
        }
        next();
      });
    },
  };
}

function normalizeHtml(html: string, options: SourceProxyOptions) {
  const { headHtml, bodyHtml, entry } = options;
  if (headHtml) {
    html = html.replace(headOccupyPosition, headHtml);
  }
  if (bodyHtml) {
    html = html.replace(bodyOccupyPosition, bodyHtml);
  }
  if (entry) {
    html = html.replace(
      entryOccupyPosition,
      `<script type="module" src="/@vite/client">
        </script><script type="module" src="${entry}?t=${Date.now()}"></script>`
    );
  }
  return html;
}

async function normalizeCss(css: string) {
  const result = await postcss([postcssRemoveEl()]).process(css);
  return result.css;
}

function postcssRemoveEl(): AcceptedPlugin {
  function isElRule({ selector }: Rule) {
    return selector.includes(".el-");
  }

  function isGlobalElRule(rule: Rule) {
    return isElRule(rule) && !rule.selector.includes("[data-v-");
  }

  return {
    postcssPlugin: "postcss-el",
    Rule(rule) {
      if (isGlobalElRule(rule)) {
        rule.remove();
      }
    },
    Declaration(decl) {
      const { prop, parent } = decl;
      if (prop.includes("--el-") && parent?.type === "rule" && !isElRule(parent as unknown as Rule)) {
        decl.remove();
      }
    },
  };
}
